package com.feinno.extas.app.util;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * 基于Redis的分布式锁
 *
 * @author zkl
 * @data 2018/09/11
 *
 * <p>
 * 基于 Redis 做分布式锁
 * 基于 redis 的 setnx()、expire() 方法做分布式锁
 * setnx()
 * setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value).该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1；如果当前 key 已经存在,则设置当前 key 失败,返回 0.
 * <p>
 * expire()
 * expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置.
 * <p>
 * 使用步骤
 * 1、setnx(lockkey, 1) 如果返回 0,则说明占位失败；如果返回 1,则说明占位成功
 * <p>
 * 2、expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题.
 * <p>
 * 3、执行完业务代码后,可以通过 delete 命令删除 key.
 * <p>
 * 这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方.
 * 比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁.
 * <p>
 * 基于 redis 的 setnx()、get()、getset()方法做分布式锁
 * 这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化.
 * <p>
 * getset()
 * 这个命令主要有两个参数 getset(key,newValue).该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值.假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:
 * <p>
 * getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1
 * getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2
 * 依次类推！
 * 使用步骤
 * 1）setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功；如果返回 0 则没有获取到锁,转向 2.
 * 2）get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3.
 * 3）计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime.
 * 4）判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁.如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试.
 * 5）在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁；如果大于锁设置的超时时间,则不需要再锁进行处理.
 * </p>
 */
public final class RedisLockUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockUtil.class);
    /**
     * 默认加锁时长是{@value DEFAULT_EXPIRE}毫秒
     */
    private static final int DEFAULT_EXPIRE = 500;
    /**
     * 默认空循环时间{@value DEFAULT_SLEEP}毫秒
     */
    private static final int DEFAULT_SLEEP = 50;
    /**
     * 默认空循环时间最大时间{@value DEFAULT_LIMIT}毫秒
     */
    private static final int DEFAULT_LIMIT = DEFAULT_EXPIRE;
    /**
     * 加锁键前缀 {@value LOCK_KEY}
     */
    private static final String LOCK_KEY = "LOCK:KEY:";
    /**
     * redis客户端
     */
    private static RedisCache redisCache;

    private RedisLockUtil() {

    }

    public static void setRedisCache(RedisCache redisCache) {
        RedisLockUtil.redisCache = redisCache;
    }

    static {
        RedisLockInit.init();
    }

    /**
     * 获取锁,才有CAS机制,获取失败则重新获取,超时返回失败
     * 相同的KEY,首次请求加锁,有锁后将不在加锁
     *
     * @param key    redis key
     * @param expire 过期时间,单位毫秒
     * @return true:加锁成功,false,加锁失败
     */
    private static boolean lock(String key, int expire, long sleep) {
        long value = System.currentTimeMillis() + expire;
        String status = redisCache.set(key, String.valueOf(value), expire);

        if ("OK".equalsIgnoreCase(status)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】,当前无锁直接获取.", key);
            }
            return true;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("key【{}】,当前有锁尝试循环获取.", key);
        }
        long limit = 0;
        int i = 0;
        for (; ; ) {
            i++;
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】,第{}次尝试获取锁.", key, i);
            }
            boolean isGet = getLock(key);
            if (isGet) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("key【{}】,第{}次尝试获取锁成功.", key, i);
                }
                return true;
            }
            if (sleep <= 0) {
                sleep = DEFAULT_SLEEP;
            }
            if (limit >= DEFAULT_LIMIT) {
                LOGGER.warn("key【{}】,第{}次获取锁失败,超过界定时间{}毫秒", new Object[]{key, i, DEFAULT_LIMIT});
                return false;
            }
            limit += sleep;
            try {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("key【{}】,第{}次获取锁失败,线程进入循环等待,休眠{}毫秒", new Object[]{key, i, sleep});
                }
                Thread.sleep(sleep);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取锁,才有CAS机制,获取失败则重新获取,超时返回失败
     *
     * @param key    redis key
     * @param expire 过期时间,单位毫秒
     * @return true:加锁成功,false,加锁失败
     */
    private static boolean lockLoop(String key, int expire, long sleep) {
        long value = System.currentTimeMillis() + expire;
        long status = redisCache.setnx(key, String.valueOf(value));

        if (status == 1) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】,当前无锁直接获取.", key);
            }
            return true;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("key【{}】,当前有锁尝试循环获取.", key);
        }
        long limit = 0;
        int i = 0;
        for (; ; ) {
            i++;
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】,第{}次尝试获取锁.", key, i);
            }
            boolean isGet = getSet(key, expire);
            if (isGet) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("key【{}】,第{}次尝试获取锁成功.", key, i);
                }
                return true;
            }
            if (sleep <= 0) {
                sleep = DEFAULT_SLEEP;
            }
            if (limit >= DEFAULT_LIMIT) {
                LOGGER.warn("key【{}】,第{}次获取锁失败,超过界定时间{}毫秒", new Object[]{key, i, DEFAULT_LIMIT});
                return false;
            }
            limit += sleep;
            try {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("key【{}】,第{}次获取锁失败,线程进入循环等待,休眠{}毫秒", new Object[]{key, i, sleep});
                }
                Thread.sleep(sleep);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 设置锁信息
     *
     * @param key
     * @param expire
     * @return
     */
    private static boolean getSet(String key, int expire) {
        String oldValue = redisCache.get(key);
        long oldExpireTime = oldValue != null ? Long.parseLong(oldValue) : 0L;
        if (oldExpireTime <= System.currentTimeMillis()) {
            //超时
            long newExpireTime = System.currentTimeMillis() + expire;
            String ExpireTime = redisCache.getSet(key, String.valueOf(newExpireTime));
            long currentExpireTime = StringUtils.isBlank(ExpireTime) ? 0 : Long.parseLong(ExpireTime);
            if (currentExpireTime == oldExpireTime) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取锁信息
     *
     * @param key
     * @return
     */
    private static boolean getLock(String key) {
        String oldValue = redisCache.get(key);
        long oldExpireTime = oldValue != null ? Long.parseLong(oldValue) : 0L;
        if (oldExpireTime <= System.currentTimeMillis()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 解锁
     *
     * @param key
     */
    private static void unLock(String key) {
        String value = redisCache.get(key);
        long oldExpireTime = value != null && value.length() > 0 ? Long.parseLong(value) : 0L;
        long currTime = System.currentTimeMillis();
        if (oldExpireTime > currTime) {
            redisCache.del(key);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】,解锁成功.", key);
            }
        } else {
            LOGGER.warn("key【{}】,锁自动过期,请检查过期时间！当前时间{},过期时间{}", key, new Object[]{currTime, oldExpireTime});
        }
    }


    /**
     * 加锁 获取锁一次
     *
     * @param key    redis key
     * @param expire 过期时间,单位毫秒
     * @return true:加锁成功,false,加锁失败
     */
    private static boolean lockOnce(String key, int expire) {
        boolean isSuccess = false;

        long value = System.currentTimeMillis() + expire;
        long status = redisCache.setnx(key, String.valueOf(value));

        if (status == 1) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.info("key【{}】,当前无锁直接获取.", key);
            }
            return true;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("key【{}】,当前有锁尝试获取.", key);
        }
        boolean isGet = getSet(key, expire);
        if (isGet) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("key【{}】,尝试获取锁成功.", key);
            }
            isSuccess = true;
        } else {
            LOGGER.warn("key【{}】,尝试获取锁失败.", key);
        }
        return isSuccess;
    }


    /**
     * 获取锁 循环
     *
     * @param id
     * @return
     */
    private static boolean lockLoop(String id) {
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String key) {
                return lockLoop(key, DEFAULT_EXPIRE, DEFAULT_SLEEP);
            }
        };
        return lock(id, lock);
    }

    /**
     * 获取锁 循环
     *
     * @param id
     * @return
     */
    private static boolean lock(String id) {
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String key) {
                return lock(key, DEFAULT_EXPIRE, DEFAULT_SLEEP);
            }
        };
        return lock(id, lock);
    }

    /**
     * 获取锁 一次
     *
     * @param id
     * @return
     */
    private static boolean lockOnce(String id) {
        id = LOCK_KEY + id;
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String k) {
                return lockOnce(k, DEFAULT_EXPIRE);
            }
        };
        return lock(id, lock);
    }

    /**
     * 获取锁 日志
     *
     * @param id
     * @return
     */
    private static boolean lock(String id, Predicate<String> lock) {
        // true表示获取锁,false标识未获取锁.如果锁未获取则不需要解锁
        boolean isLock = false;
        try {
            long end;
            long start = System.currentTimeMillis();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("业务ID:{},准备获取锁", id);
            }
            // 加锁失败返回失败,防止堵塞
            if (lock.test(id)) {
                if (LOGGER.isDebugEnabled()) {
                    end = System.currentTimeMillis();
                    LOGGER.debug("业务ID:{},获取锁成功,加锁执行时长{}毫秒", id, (end - start));
                }
                isLock = true;
            } else {
                end = System.currentTimeMillis();
                LOGGER.error("业务ID:{},获取锁失败,加锁执行时长{}毫秒", id, (end - start));
            }
        } catch (Exception e) {
            LOGGER.error("业务ID:{},获取锁异常", e);
        }
        return isLock;
    }

    /**
     * 解锁
     *
     * @param id
     * @param time
     */
    private static void unlock(String id, long time) {
        try {
            unLock(id);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("业务ID:{},解锁成功,业务执行时长{}毫秒", id, (System.currentTimeMillis() - time));
            }
        } catch (Exception e) {
            LOGGER.error("业务ID:{},解锁异常", e);
        }
    }

    /**
     * 基于Redis分布式锁执行,循环
     *
     * @param key    加锁key
     * @param handle 执行内容
     * @return
     */
    public static void useLockLoop(String key, LockHandle handle) {
        key = LOCK_KEY + key;
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String k) {
                return lockLoop(k);
            }
        };
        useLock(key, lock, handle);
    }

    /**
     * 基于Redis分布式锁执行,循环,加锁一次,解锁后剩余的统一释放,不在加锁
     *
     * @param key    加锁key
     * @param handle 执行内容
     * @return
     */
    public static void userLockUnifiedRelease(String key, LockHandle handle) {
        key = LOCK_KEY + key;
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String k) {
                return lock(k);
            }
        };
        useLock(key, lock, handle);
    }

    /**
     * 基于Redis分布式锁执行,一次,如果有锁跳过不执行
     *
     * @param key    加锁key
     * @param handle 执行内容
     * @return
     */
    public static void userLockOnce(String key, LockHandle handle) {
        key = LOCK_KEY + key;
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String k) {
                return lockOnce(k);
            }
        };
        useLock(key, lock, handle);
    }

    /**
     * 基于Redis分布式锁执行,一次,延迟解锁
     *
     * @param key    加锁key
     * @param handle 执行内容
     * @return
     */
    public static void userLockOnceDelayUnLock(String key, LockHandle handle) {
        key = LOCK_KEY + key;
        //TODO JDK兼容性所以没使用lambda表达式
        Predicate<String> lock = new Predicate<String>() {
            @Override
            public boolean test(String k) {
                return lockOnce(k);
            }
        };
        userLockDelayUnlock(key, lock, handle);
    }

    /**
     * 获取成功后并解锁
     *
     * @param key
     * @param lock
     * @param handle
     */
    private static void useLock(String key, Predicate<String> lock, LockHandle handle) {
        long start = 0;
        // true表示获取锁,false标识未获取锁.如果锁未获取（获取失败）则不需要解锁
        boolean isLock = false;
        try {
            isLock = lock.test(key);
            if (!isLock) {
                return;
            }
            start = System.currentTimeMillis();

            //业务部分
            handle.handle();
        } finally {
            if (isLock) {
                unlock(key, start);
            }
        }
    }

    /**
     * 获取成功后不解锁
     *
     * @param key
     * @param lock
     * @param handle
     */
    private static void userLockDelayUnlock(String key, Predicate<String> lock, LockHandle handle) {
        long start = 0;
        // true表示获取锁,false标识未获取锁.如果锁未获取（获取失败）则不需要解锁
        boolean isLock = false;
        try {
            isLock = lock.test(key);
            if (!isLock) {
                return;
            }
            start = System.currentTimeMillis();

            //业务部分
            handle.handle();
        } catch (Exception e) {
            if (isLock) {
                unlock(key, start);
            }
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 执行方法
     */
    public interface LockHandle {
        void handle();
    }

    /**
     * Redis锁涉及的方法
     */
    public interface RedisCache {
        String set(String k, String v, long seconds);

        long setnx(String k, String v);

        String getSet(String k, String v);

        String get(String k);

        long del(String k);
    }
}